//! File system implementation.  Five layers:
//!   + Blocks: allocator for raw disk blocks.
//!   + Log: crash recovery for multi-step updates.
//!   + Files: inode allocator, reading, writing, metadata.
//!   + Directories: inode with special contents (list of other inodes!)
//!   + Names: paths like /usr/rtm/xv6/fs.c for convenient naming.
//!
//! This file contains the low-level file system manipulation routines.
//! The (higher-level) system call implementations are in syscall/file.zig.

const std = @import("std");
const os = @import("os.zig");

pub const Buffer = @import("fs/Buffer.zig");
pub const log = @import("fs/log.zig");
pub const Inode = @import("fs/Inode.zig");
pub const File = @import("fs/File.zig");
pub const Pipe = @import("fs/Pipe.zig");
pub const path = @import("fs/path.zig");

pub const fd_t = u32;
pub const off_t = u32;

/// root i-number
pub const ROOTINO = 1;
/// block/sector size
pub const BSIZE = 1024;

/// Number of data blocks addressable through ONE indirect block
pub const blocks_per_indirect = BSIZE / @sizeOf(Block);
pub const blockidx = Inode.Dinode.blockidx;
pub const MAXFILE = Inode.Dinode.total_blocks;

/// Inodes per block.
pub const IPB = BSIZE / @sizeOf(Inode.Dinode);

/// Bitmap bits per block
pub const BPB = BSIZE * @bitSizeOf(u8);

/// max # of blocks any FS op writes
pub const MAXOPBLOCKS = 10;
/// max data blocks in on-disk log
pub const LOGBLOCKS = MAXOPBLOCKS * 3;
/// size of disk block cache
pub const NBUF = MAXOPBLOCKS * 3;

/// maximum file path name
pub const MAXPATH = 128;

// there should be one superblock per disk device, but we run with only one device
pub var sblock: SuperBlock = undefined;
pub var devsw: [os.NDEV]DevSw = @splat(.{});

/// Initializes core filesystem infrastructure that can't sleep.
pub fn init(_: ?*const anyopaque) void {
    Buffer.init(); // buffer cache

    // Performs late filesystem initialization. Must be called from process context due to sleep usage.
    // Cannot be used during kernel initialization before process scheduling starts.
    sblock = .read();
    log.init(&sblock);
}

// map major device number to device functions.
const DevSw = struct {
    read: ?*const fn (usize, usize) anyerror!usize = null,
    write: ?*const fn (usize, usize) anyerror!usize = null,
};

pub const SeekMode = enum(u8) {
    /// The file offset is set to offset bytes.
    set,
    /// The file offset is set to its current location plus offset bytes.
    cur,
    /// The file offset is set to the size of the file plus offset bytes.
    end,
};

/// File open flags. Combinations use bitwise OR.
pub const O = packed struct(u32) {
    access: ACCMODE = .RDONLY,
    _2: u6 = 0,

    no_follow: bool = false,
    create: bool = false,
    truncate: bool = false,
    append: bool = false,

    _: u20 = 0,

    // POSIX has a note about the access mode values:
    //
    // In historical implementations the value of O_RDONLY is zero. Because of that, it is not possible
    // to detect the presence of O_RDONLY and another option. Future implementations should encode
    // O_RDONLY and O_WRONLY as bit flags so that: O_RDONLY | O_WRONLY == O_RDWR
    pub const ACCMODE = enum(u2) {
        NONE = 0,
        RDONLY = 1,
        WRONLY = 2,
        RDWR = 3,
    };
};

// Disk layout:
// [ boot block | super block | log | inode blocks | free bit map | data blocks]
//
// mkfs computes the super block and builds an initial file system. The
// super block describes the disk layout:
pub const SuperBlock = struct {
    pub const FSMAGIC = 0x10203040;

    /// Must be FSMAGIC
    magic: u32 = FSMAGIC,
    /// Size of file system image (blocks)
    size: u32,
    /// Number of data blocks
    nblocks: u32,
    /// Number of inodes.
    ninodes: u32,
    /// Number of log blocks
    nlog: u32,
    /// Block number of first log block
    logstart: u32,
    /// Block number of first inode block
    inodestart: u32,
    /// Block number of first free map block
    bmapstart: u32,

    comptime {
        std.debug.assert(@sizeOf(SuperBlock) < BSIZE);
    }

    pub fn read() SuperBlock {
        const bp: *Buffer = .read(.super);
        defer bp.release();

        var s: SuperBlock = undefined;
        @memcpy(std.mem.asBytes(&s), bp.data[0..@sizeOf(SuperBlock)]);

        if (s.magic != FSMAGIC) @panic("invalid file system");
        return s;
    }

    /// Block containing inode i
    pub fn inodeBlock(s: SuperBlock, i: u32) Block {
        return .init(s.inodestart + i / IPB);
    }

    /// Block of free map containing bit for block b
    pub fn bitmapBlock(s: SuperBlock, b: u32) Block {
        return .init(s.bmapstart + b / BPB);
    }
};

pub const Block = packed struct(u32) {
    index: u32 = 0,

    pub const empty: Block = .init(0);
    pub const super: Block = .init(1);

    const BitMap = std.bit_set.ArrayBitSet(u8, BPB);

    comptime {
        std.debug.assert(@bitSizeOf(BitMap) == @bitSizeOf(@FieldType(Buffer, "data")));
    }

    pub fn init(number: u32) Block {
        return @bitCast(number);
    }

    pub fn add(self: Block, offset: usize) Block {
        return .init(@intCast(self.index + offset));
    }

    // Zero a block.
    fn zero(block: Block) void {
        const buf: *Buffer = .read(block);
        defer buf.release();

        @memset(&buf.data, 0);
        log.write(buf);
    }

    /// Allocate a zeroed disk block, return error if out of disk space. Also mark the relevant bit in
    /// bitmap to 1.
    pub fn alloc() error{NoSpaceLeft}!Block {
        var blockno: u32 = 0;
        const allocated = while (blockno < sblock.size) : (blockno += BPB) {
            var bitmap_buf: *Buffer = .read(sblock.bitmapBlock(blockno));
            defer bitmap_buf.release();

            var bitmap: *BitMap = @ptrCast(&bitmap_buf.data);
            var iter = bitmap.iterator(.{ .kind = .unset }); // look up free block
            const index: u32 = @intCast(iter.next() orelse continue);
            const result = blockno + index;
            if (result >= sblock.size) break 0;

            bitmap.set(index); // Mark block in use.
            log.write(bitmap_buf);
            break result;
        } else 0;

        if (allocated == 0) {
            std.log.scoped(.fs).err("{s}: out of blocks\n", .{@src().fn_name});
            return error.NoSpaceLeft;
        }

        var block: Block = .init(allocated);
        block.zero();
        return block;
    }

    /// Free a disk block. Also mark the relevant bit in bitmap to 0.
    pub fn free(block: Block) void {
        const blockno: u32 = block.index;
        std.debug.assert(blockno > 0);
        var bitmap_buf: *Buffer = .read(sblock.bitmapBlock(blockno));
        defer bitmap_buf.release();

        var bitmap: *BitMap = @ptrCast(&bitmap_buf.data);
        if (!bitmap.isSet(blockno % BPB)) @panic("freeing free block");
        bitmap.unset(blockno % BPB);
        log.write(bitmap_buf);
    }
};
